#coding:utf-8
import weakref
import traceback
import time
import threading
import os

import __builtin__ as gl


from modules import profilesManager
from modules import pluginsManager as plug
from modules import pipe
from modules import threadPool
from modules import botsManager as BotManager
from modules import heartBeat
from modules import derrived


from modules import utils

from twisted.internet.task import LoopingCall

from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber import client
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.xmlstream import IQ, TimeoutError

from twisted.internet import reactor
from twisted.internet import protocol
from twisted.internet import defer
from twisted.internet.threads import deferToThread

from twisted.words.xish import domish

gl.ROLES = {
	"none"       : 0,
	"visitor"    : 0,
	"participant": 10,
	"moderator"  : 15
}

gl.AFFILIATIONS = {
	"outcast" : 0,
	"none"    : 0,
	"member"  : 1,
	"admin"   : 5,
	"owner"   : 15
}

class chatUser(object):
	'''
		Контейнер хранящий в себе некоторые данные о пользователе который находится в чате
	'''
	__slots__ = ("__dict__","jid","isHere","join")
	def __init__(self,jid=None,access=0,nick=None,aff=None,role=None):
		self.jid = jid
		self.access = access
		self.nick = nick
		self.aff = aff
		self.role = role
		self.join = None
		self.idle = None
		self.isHere = False
	def online(self):
		self.join = time.time()
		self.isHere = True
	def offline(self):
		self.isHere = False
                self.__dict__.clear()
	def update(self):
		'''
			обновленияе времени последней активность
		'''
		self.idle = time.time()
	def copy(self):
		'''
			возвращает копию контейнера
		'''
		import copy
		return copy.copy(self)

class Subject(object):
	'''
		Контейнер агрегирующий объект стансы с объектом клиента
	'''
	__slots__ = ("__xml","__client")
	def __init__ (self,cl,xml):
		self.__xml = xml
		self.__client = client
	def __getattr__(self,name):
		'''
			перегруженная функция вызова методов клиента
		'''
		return self.__client.__getattr__(name)
	def element(self):
		'''
			возвращает объект стансы
		'''
		return self.__xml

def isState(predicate):
	'''
		декоратор "семафор" проверающий состояние клиента
		для разрешения выполнения функции
	'''
	assert callable(predicate)
	def call(func):
		def wrapper(self,*args,**kwargs):
			if predicate(self.state):
				return func(self,*args,**kwargs)
		return wrapper
	return call




def callEvent(eventType,mode,*args,**kwargs):
	deferList = []
	if mode is "deffered":
		for func in plug.getEvents(eventType):
			_defer = defer.Deferred()
			_defer.addCallback(func)
			_defer.callback(*args,**kwargs)
			deferList.append(_defer)
		_deferList = defer.DeferredList(deferList)
		return _deferList
	elif mode is "threadDeferred":
		for func in plug.getEvents(eventType):
			_defer = deferToThread(func,*args,**kwargs)
			deferList.append(_defer)
		_deferList = defer.DeferredList(deferList)
		return _deferList
	elif mode is "sync":
		for func in plug.getEvents(eventType):
			func(*args,**kwargs)
	elif mode is "queue":
		for func in plug.getEvents(eventType):
			tPool.start(lambda:func(*args,**kwargs))


def callCommand(commandHandler,*args,**kwargs):
	pass

def Bootstrap(xpath):
        def wrapper(func):
                func._bootstrap_func = xpath
                return func
        return wrapper

def Observer(xpath):
        def wrapper(func):
                func._observer_func = xpath
                return func
        return wrapper

class KernelBot(derrived.Derrived):
	def __init__(self,profile):
		self.profile = profile #настройки подключения и профиль в целом
		self.conferences = {} #словарь конференций в которых находится бот и их юзеры
		self.__xmlStreamSession = None #тут лежит ссылка на объект сессии
		self.__connection = None #ссылка самого подключения
		self.__state = "init" #состояние клиент
		self.selfJid = jid.JID(profile.FULL_JID)
		self.globalLock = None
		self.mainConfig = None
		self.conferencesConfigs = {}
		self.profile = profile
		self.xping = heartBeat.HeartBeat(self) # запуск пинговалки аля KeepAlive
		print self.selfJid
		self.clientFactory=client.XMPPClientFactory(self.selfJid,self.profile.PASSWORD) #инициализируем XMPP клиент
		import inspect
		for name,handle in inspect.getmembers(self,callable):
                        if hasattr(handle,"_bootstrap_func"):
                                self.clientFactory.addBootstrap(handle._bootstrap_func,handle)
		self.clientFactory.clientConnectionLost = self.connectionLost #соединение потеряно
		self.clientFactory.clientConnectionFailed = self.connectionFailed #ошибка соединения
		self.eventDisconnect = None
		self.eventConnect = None
	@property
	def xmlstream(self):
		'''
			так как __xmlStreamSession есть слабая ссылка, то для извлечения объекта необходимо вызвать
			эту ссылку,для упрощения работы + гарантия целостности  read-only. readme weakref
		'''
		return self.__xmlStreamSession
	@property
	def state(self):
		'''
			возврат статуса клиента
		'''
		return self.__state

	def getFullPath(self,*args):
		return os.path.join("databases",*args)
		
        @Bootstrap("//event/stream/authd")
	def _authSucess(self,xmlstream):
		'''
			результат успешной аутентификации
		'''
                import inspect
		print "AUTH OK!"
		self.globalLock = threading.Lock()
		self.__state = "connect"
		self.send(self.createPresenceNode(priority = self.profile.PRIORITY)) #установка статуса и приоритета ресурса поделючения
		self.__xmlStreamSession = weakref.proxy(xmlstream) #создаём слабую ссылку сессии
                for name,handle in inspect.getmembers(self,callable):
                        if hasattr(handle,"_observer_func"):
                                self.__xmlStreamSession.addObserver(handle._observer_func,handle)
                def initBot():
                        with self.globalLock:
                                self.mainConfig = utils.loadConfig("configs/main_config.ini",
                                                {"conferences":[],
                                                  "cmdacces":{},
                                                  "accesses":{},
                                                  "modulesconfig":{}})
                                callEvent(plug.EVENT_STARTUP,"sync")
                                for conference in iter(self.mainConfig["conferences"]):
                                        self.initConference(conference)
                                        self.joinConference(conference)
                                        callEvent(plug.EVENT_INITCONFERENCE,"sync",conference)
                                self.xping.start()
                reactor.callInThread(initBot)
        @Bootstrap("//event/xmpp/initfailed")
	def _authFail(self,xmlstream):
		print "AUTH FAIL"
        @Bootstrap("//event/stream/connected")
	def _onConnected(self,xmlstream):
		self.__state = "establish_link"
        @Bootstrap("//event/stream/end")
	@isState(lambda st: st!="disconnect")
	def _endStream(self,xmlstream):
		'''
			<stream:stream/>
		'''
		self.disconnect()
        @Bootstrap("//event/stream/error")
	def _errorStream(self,xmlstream):
		'''
			Обработка ошибки сессии
		'''
		try:
			el = xmlstream.value.getElement()
		except:
			print "STREAM ERROR"
			return
		if el.firstChildElement().name == 'conflict':
			reactor.callLater(0.5,self.connect) # в случае если ошибка "Resource conflict" то через 0.5 сек поделючение

	@isState(lambda st: st!="disconnect")
	def connectionLost(self,connector,reason):
		'''
			Обработка потери поделючения
		'''
		print "Lol"
		self.disconnect()

	def connectionFailed(self,connector,reason):
		'''
			Обработка ошибки поделючения
		'''
		print reason
		if self.__state == "init": #HACK
			self.connectionLost(connector,reason)
			return
                self.__state = "disconnect"
		reactor.callLater(10,self.connect)

	@isState(lambda st: st!="disconnect")
	def disconnect(self,safeConnect = True,restart = False):
		'''
			Отключение
			Закрытие сессии
			Закрытие поделючения
		'''
		print "Disconnect"
		self.__state = "disconnect"
		try:
			self.xping.stop() # вырубаем пинговалку
		except Exception:
			pass
		self.clientFactory.stopTrying() # закрытие сессии
		if self.__connection:
			self.__connection.disconnect() # закрытие подключение
			if not safeConnect:
				self.__connection = None
		if restart:
			self.__state = "reconnect"
			reactor.callLater(1,self.connect)
		if callable(self.eventDisconnect):
			reactor.callFromThread(self.eventDisconnect,self)
		return callEvent(plug.EVENT_SHUTDOWN,"deferToThread",self)

	@isState(lambda st: st in ("disconnect","init"))
	def connect(self):
		'''
			Функция подключения
		'''
		self.__state = "do_connect"
		if self.__connection:
			self.__connection.connect()
		else:
			if self.profile.SSL:
                                pass
				from twisted.internet import ssl
				self.__connection=reactor.connectSSL(self.profile.HOST,self.profile.PORT,self.clientFactory,ssl.ClientContextFactory())
			else:
				self.__connection=reactor.connectTCP("c4.ciliz.com",5222,self.clientFactory)
                print "Do connect"

        def initConference(self,conference):
                        self.conferences[conference] = {}
                        self.loadConferenceConfig(conference)
                         #callEvent()
                         
        def loadConferenceConfig(self,conference):
                self.conferencesConfigs[conference] = utils.loadConfig(os.path.join("configs",conference+".ini"),{
                                "nick":self.profile.NICK,
                                "password":None,
                                "status":"dnd",
                                "show":"online",
                                "priority":0,
                                "accesses":{},
                                "modulesconfig":{},
                                "cmdacces":{}})
                                
        def freeConference(self,conference):
                self.conferencesConfigs[conference].write()
                del self.conferencesConfigs[conference]
                del self.conferences[conference]
                
	def send(self,subject,to=None):
		'''
			Отправка стансы
		'''
		if to and not isinstance(subject,basestring):
			subject["to"] = to
		if threading.currentThread() is threading._MainThread():
                        self._send(subject)
                else:
                        reactor.callFromThread(self._send,subject)

	@isState(lambda st: st =="connect")
	def _send(self,subject):
		if self.xmlstream:
			try:
				self.xmlstream.send(subject)
			except Exception:
				print traceback.format_exc()
				
	def getAccess(self,jid,conference = None):
                if jid and jid in self.mainConfig["accesses"]:
                        return self.mainConfig["accesses"][jid]
		if conference:
			if self.conferencesConfigs[conference]["accesses"].has_key(jid):
                                return self.conferencesConfigs[conference]["accesses"]["jid"]
		else:
			return 10
		return 0

	def IQ(self,typ="set",to=None,timeout=None):
		iq = IQ(self.xmlstream,typ)
		iq.timeout = timeout
		if to:
			iq["to"] = to
		return iq

	def sendMsg(self,msgType,msgTo,msgPayload,nick = None):
		msg=domish.Element(('jabber:client', 'message'))
		msg["to"]=msgTo
		msg["type"]=msgType
		msg.addElement("body",content=(nick and u"{nick}, {msgPayload}".format(**locals()) or msgPayload))
		self.send(msg)
		
        @Observer("/iq")
        def onIqParse(self,element):
                if not element.hasAttribute("from"):
                        return
                if element.hasAttribute("id"):
                        iqResponse = domish.Element((None,"iq"))
                        iqResponse["to"] = element["from"]
                        iqResponse["id"] = element["id"]
                        if element.getAttribute("type") == "get":
                                childUri  = element.firstChildElement() and element.firstChildElement().uri or None
                                if childUri == "urn:xmpp:ping":
                                        iqResponse["type"] = "result"
                                        self.send(iqResponse)
                                        return
                        elif element["id"] in self.xmlstream.iqDeferreds and element.getAttribute("type") in ("result","error"):
                                return
                        iqResponse["type"]  = "error"
                        iqResponse["from"] = self.profile.FULL_JID
                        err = iqResponse.addElement("error")
                        err["code"] = "501"
                        err["type"] = "cancel"
                        err.addElement("feature-not-implemented")
                        self.send(iqResponse)
                else:
                        element["type"]  = "error"
                        element["to"] = el["from"]
                        element["from"] = self.profile.FULL_JID
                        err = element.addElement("error")
                        err["code"] = "400"
                        err["type"] = "modify"
                        err.addElement("bad-request")
                        self.send(element)
                        
			#callEvent(plug.EVENT_PRESENCE | plug.H_CONFERENCE,"queue",self,element,conference,nick,userJid)###FIXME!
			
	@Observer("/presence[not(@type)]/x[@xmlns='http://jabber.org/protocol/muc#user']")
	def onPresenceJoin(self,element):
                _jid = jid.JID(element["from"])
                conference = _jid.userhost()
                nick = _jid.resource
                if not self.conferences.has_key(conference):
                        return
                prsNode = element | utils.getTag("x","http://jabber.org/protocol/muc#user")
                prsItems = prsNode | utils.getTag("item")
                userJid =  prsItems.getAttribute("jid")
                if userJid:
                        userJid = jid.JID(userJid).userhost()
                affiliation = prsItems.getAttribute("affiliation")
                role = prsItems.getAttribute("role")
                item = chatUser(userJid,max(self.getAccess(userJid,conference),AFFILIATIONS[affiliation]+ROLES[role]),nick,affiliation,role)
                item.online()
                self.conferences[conference][nick] = item
                callEvent(plug.EVENT_USERONLINE,"queue",Subject(self,element),conference,nick,jid,affiliation,role)
                
        @Observer("/presence[@type='unavailable']/x[@xmlns='http://jabber.org/protocol/muc#user']")       
        def onPresenceLeave(self,element):
                _jid = jid.JID(element["from"])
                conference = _jid.userhost()
                nick = _jid.resource
                if not self.conferences[conference].has_key(nick):
                        return
                prsNode = element | utils.getTag("x","http://jabber.org/protocol/muc#user")
                prsItems = prsNode | utils.getTag("item")
                statusCode,status = prsNode | utils.getStatusPayload()
                reason = (prsNode | utils.getTag("reason") | utils.__str__) or status
                userJid =  prsItems.getAttribute("jid")
                if statusCode == "303":
                        newNick = prsItems.getAttribute("nick")
                self.conferences[conference][nick].offline()
                callEvent(plug.EVENT_USEROFFLINE,"queue",Subject(self,element),conference,nick,userJid,reason,statusCode)
                
        @Observer("/presence[@type='error']/x[@xmlns='http://jabber.org/protocol/muc']")
        def onPresenceError(self,element):
                errorNode = element | utils.getTag("error")
                errorCode = errorNode["code"]
                reasonError = errorNode | utils.getTag("text") or ""
                if reasonError :
                        reasonError = unicode(reasonError)
                print errorCode,reasonError
                
	@Observer("/message[@type='chat']/body")		
        def onRosterMessage(self,element):
                msgFullJid = jid.JID(element["from"])
                bareJid = msgFullJid.userhost()
                if self.conferences.has_key(bareJid) :
                        self.onGroupchatMessage(element)
                        return
                userAccess = self.getAccess(bareJid)
                if userAccess == -100:       
                        return
                if element | utils.getTag("request"):
                        msgReport = domish.Element((None,"message"))
                        msgReport["to"] = element["from"]
                        msgReport["id"] = element.getAttribute("id")
                        msgReport.addElement("received","urn:xmpp:receipts")
                        self.send(msgReport)
                messageContent = element | utils.getTag("body")| utils.__str__
                messageContent = messageContent.strip()
                if not messageContent:
                        return
                command,params = messageContent | utils.splitMessageQuery
                if plug.isCommand(command):
                        self.__callCommand(bareJid,"chat",plug.CMD_ROSTER,userAccess,command,params)
                        
        @Observer("/message[@type='groupchat']/body")
        def onGroupchatMessage(self,element):
                msgFullJid = jid.JID(element["from"])
                bareJid = msgFullJid.userhost()
                if not self.conferences.has_key(bareJid):
                        return
                userItem = self.conferences[bareJid].get(msgFullJid.resource)
                if userItem is None or userItem.access == -100:
                        return
                messageContent = element | utils.getTag("body")| utils.__str__
                messageContent = messageContent.strip()
                command,params = messageContent | utils.splitMessageQuery
                if plug.isCommand(command):
                        self.__callCommand(bareJid,element["type"],plug.CMD_CHAT,userItem.access,command,params)
                        
        @Observer("/message[@type='error']/error[@code='406']")
        def onErrorMessage406(self,element):
                def unstoppable():
                        fromErr = jid.JID(element["from"])
                        self.initConference(fromErr.userhost())
                        self.joinConference(fromErr.userhost())
                        reactor.callLater(0.5,self._repeatOnError,element)
                reactor.callInThread(unstoppable)
                
        @Observer("/message[@type='error']/error[@code='500']")
        def onErrorMessage500(self,element):
                reactor.callLater(1,self._repeatOnError,element)
                
        def _repeatOnMsgError(self,element):
                messageReply = domish.Element((None,"message"))
                messageReply["to"] = element["to"]
                messageReply["from"] = element["from"]
                body = messageReply.addElement("body",messageContent)
                self.send(messageReply)
                
        def __callCommand(self,bareJid,msgType,conType,access,command,params):
                cmdHandle = plug.Commands[command]
                if conType & cmdHandle.cmdType:
                        if cmdHandle.access <= access:
                                        if not params and cmdHandle.cmdType & plug.CMD_PARAM:
                                                self.sendMsg(msgType,bareJid,u"Для этой команды необходимы параметры\nВоспользуйтесь помощью по команде!")
                                                return
                                        if params and cmdHandle.cmdType & plug.CMD_NONPARAM:
                                                self.sendMsg(msgType,bareJid,u"Для этой команды не требуются параметры\nВоспользуйтесь помощью по команде!")
                                                return
                                        tPool.start(lambda:cmdHandle(self,msgType,bareJid,params))
                        else:
                                self.sendMsg(msgType,bareJid,u"Недостаточно прав")

def run():

	globals()["tPool"] = threadPool.ThreadPool(lifeTime = 5,maxSize = 1)
	plug.LoadPlugins("plugins")
	profilesManager.profileReg("profiles")
	for pr in profilesManager.getProfileIter():
		k = KernelBot(pr)
		BotManager.registry(k)
		k.connect()
	reactor.run()

if __name__ == "__main__":
	run()